/**
 * (C) Copyright MarketLive. 2007. All rights reserved.
 * MarketLive is a trademark of MarketLive, Inc.
 * Warning: This computer program is protected by copyright law and international treaties.
 * Unauthorized reproduction or distribution of this program, or any portion of it, may result
 * in severe civil and criminal penalties, and will be prosecuted to the maximum extent
 * possible under the law.
 */
package com.marketlive.system.keymanagement;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.interfaces.RSAPrivateKey;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.ShortBufferException;
import javax.crypto.spec.PBEParameterSpec;

import org.apache.commons.codec.binary.Base64;

/**
 * This class is responsible for performing the encryption,
 * decryption and hashing cryptographic functions.  All
 * methods are static.
 */
public class CryptoManager {
    
    public static final String HASH_TYPE = "SHA-256";
    public static final String PBE_ALGORITHM = "PBEWithSHA1AndDESede";
    public static final String RSA_KEY_TYPE = "RSA";
    public static final String RSA_CRYPTO_ALGORITHM="RSA/ECB/PKCS1Padding";
    
    /**
     * Constructor
     */
    public CryptoManager() {
        // EMPTY
    }
    
    /**
     * Encrypts a byte array using a symmetric encryption algorithm.
     * This method is useful for encrypting anything other than a String.
     * 
     * @param byte[] input that needs to be encrypted
     * @param byte[] iv The initialization vector for block encryption
     * @param byte[] key The symmetric encryption key
     * @return String The base-64 encoded encrypted string
     */
    public String encrypt(byte[] input, byte[] iv, byte[] key)
            throws Exception {
        return null;
    }
    
    /**
     * Overloaded method that encrypts a String using a symmetric 
     * encryption algorithm
     * 
     * @param String input that needs to be encrypted
     * @param byte[] iv The initialization vector for block encryption
     * @param byte[] key The symmetric encryption key
     * @return String The base-64 encoded encrypted string
     */
    public String encrypt(String input, byte[] iv, byte[] key)
            throws Exception {
        return null;
    }
    
    /**
     * Overloaded method that encrypts a byte array using PBE 
     * 
     * @param byte[] input that needs to be encrypted
     * @param byte[] pbekey The PBE key
     * @param String salt for creating a random salt for the keyspec
     * @param short iterations The number of times the keycode is processed 
     * @return String The base-64 encoded encrypted string
     * @throws NoSuchPaddingException 
     * @throws NoSuchAlgorithmException 
     * @throws InvalidKeyException 
     * @throws BadPaddingException 
     * @throws IllegalBlockSizeException 
     * @throws UnsupportedEncodingException 
     * @throws InvalidAlgorithmParameterException 
     * @exception
     */
    public String encrypt(byte[] input, SecretKey pbekey, String salt, short iterations) 
            throws NoSuchAlgorithmException, NoSuchPaddingException, 
            InvalidKeyException, IllegalBlockSizeException, 
            BadPaddingException, InvalidAlgorithmParameterException, 
            UnsupportedEncodingException {
        
        // Create the Cipher and initialize it
        Cipher c = Cipher.getInstance(PBE_ALGORITHM);
        c.init(Cipher.ENCRYPT_MODE, pbekey, 
                new PBEParameterSpec(salt.getBytes("UTF-8"), iterations));
        
        // Encrypt input
        byte[] ciphertext = c.doFinal(input);
        
        // Base64 encode ciphertext and return string
        return new String(Base64.encodeBase64(ciphertext));
    }
    
    /**
     * Method encrypts a byte array using an RSA Public Key and 
     * the RSA_CRYPTO_ALGORITHM
     * 
     * @param byte[] plaintext that needs to be decrypted
     * @param PublicKey pbk The RSA Public Key
     *  
     * @return String The encrypted, base64-encoded ciphertext
     * 
     * @throws NoSuchPaddingException 
     * @throws NoSuchAlgorithmException 
     * @throws InvalidKeyException 
     * @throws BadPaddingException 
     * @throws IllegalBlockSizeException  
     * @throws ShortBufferException 
     * @throws InvalidAlgorithmParameterException 
     * @exception
     */
    public String rsaEncrypt(byte[] plaintext, PublicKey pbk) 
            throws NoSuchAlgorithmException, NoSuchPaddingException, 
            InvalidKeyException, IllegalBlockSizeException, 
            BadPaddingException, ShortBufferException {
        
        // Create the Cipher and initialize it
        Cipher c = Cipher.getInstance(RSA_CRYPTO_ALGORITHM);
        c.init(Cipher.ENCRYPT_MODE, pbk);
        
        // Encrypt plaintext
        byte[] ctbytes = new byte[c.getOutputSize(plaintext.length)];
        int ctlen = c.update(plaintext, 0, plaintext.length, ctbytes, 0);
        ctlen = c.doFinal(ctbytes, ctlen);
        byte[] b64ct = Base64.encodeBase64(ctbytes);
                
        // Decrypt and return plaintext
        return new String(b64ct);
    }
    
    /**
     * Decrypts a byte array using a symmetric-key decryption algorithm
     * 
     * @param byte[] input that needs to be decrypted
     * @param byte[] iv The initialization vector for block encryption
     * @param byte[] key The symmetric encryption key
     * @return byte[] 
     */
    public byte[] decrypt(byte[] input, byte[] iv, byte[] key)
            throws Exception {
        return null;
    }
    
    /**
     * Decrypts a Base64-encoded string using a symmetric-key decryption 
     * algorithm.  Method decodes the string before decrypting it.
     * 
     * @param byte[] input that needs to be decrypted
     * @param byte[] iv The initialization vector for block encryption
     * @param byte[] key The symmetric encryption key
     * @return byte[] 
     */
    public byte[] decrypt(String input, byte[] iv, byte[] key)
            throws Exception {
        return null;
    }
    
    /**
     * Overloaded method that decrypts a byte array using PBE 
     * 
     * @param String ciphertext Base64 ciphertext that needs to be decrypted
     * @param byte[] pbekey The PBE key
     * @param String salt for creating a random salt for the keyspec
     * @param short iterations The number of times the keycode is processed 
     * @return String The plaintext string
     * @throws NoSuchPaddingException 
     * @throws NoSuchAlgorithmException 
     * @throws InvalidKeyException 
     * @throws BadPaddingException 
     * @throws IllegalBlockSizeException 
     * @throws UnsupportedEncodingException 
     * @throws InvalidAlgorithmParameterException 
     * @exception
     */
    public String decrypt(byte[] ciphertext, SecretKey pbekey, String salt, short iterations) 
            throws NoSuchAlgorithmException, NoSuchPaddingException, 
            InvalidKeyException, IllegalBlockSizeException, 
            BadPaddingException, InvalidAlgorithmParameterException,
            UnsupportedEncodingException {
        
        // Create the Cipher and initialize it
        Cipher c = Cipher.getInstance(PBE_ALGORITHM);
        c.init(Cipher.DECRYPT_MODE, pbekey,
                new PBEParameterSpec(salt.getBytes("UTF-8"), iterations));
        
        // Convert base64 text to raw bytes
        byte[] ctbytes = Base64.decodeBase64(ciphertext);
        
        // Decrypt and return plaintext
        byte[] plaintext = c.doFinal(ctbytes);
        return new String(plaintext);
    }
    
    /**
     * Method decrypts a base64-encoded byte array using an RSA
     * PrivateKey and the RSA_CRYPTO_ALGORITHM
     * 
     * @param String ciphertext Base64 ciphertext that needs to be decrypted
     * @param PrivateKey pvk The RSA Private Key
     *  
     * @return byte[] The decrypted plaintext
     * @throws NoSuchPaddingException 
     * @throws NoSuchAlgorithmException 
     * @throws InvalidKeyException 
     * @throws BadPaddingException 
     * @throws IllegalBlockSizeException 
     * @throws UnsupportedEncodingException 
     * @throws InvalidAlgorithmParameterException 
     * @exception
     */
    public byte[] rsaDecrypt(String ciphertext, PrivateKey pvk) 
            throws NoSuchAlgorithmException, NoSuchPaddingException, 
            InvalidKeyException, IllegalBlockSizeException, 
            BadPaddingException, UnsupportedEncodingException {
        
        // Create the Cipher and initialize it
        Cipher c = Cipher.getInstance(RSA_CRYPTO_ALGORITHM);
        c.init(Cipher.DECRYPT_MODE, pvk);
        
        // Convert base64 text to raw bytes
        byte[] ctbytes = Base64.decodeBase64(ciphertext.getBytes("utf-8"));
        
        // Decrypt and return plaintext
        return c.doFinal(ctbytes);
    }
    
    /**
     * Unwraps an encrypted private key (obviously there's no need to
     * encrypt a public key) 
     * 
     * @param String ciphertext Base64 ciphertext that needs to be decrypted
     * @param byte[] pbekey The PBE key
     * @param String salt for creating a random salt for the keyspec
     * @param short iterations The number of times the keycode is processed 
     * @return Key The plaintext RSA key
     * 
     * @throws NoSuchPaddingException 
     * @throws NoSuchAlgorithmException 
     * @throws InvalidKeyException 
     * @throws UnsupportedEncodingException 
     * @throws InvalidAlgorithmParameterException 
     * 
     */
    public Key unwrap(byte[] ciphertext, SecretKey pbekey, String salt, short iterations) 
            throws NoSuchAlgorithmException, NoSuchPaddingException, 
            InvalidKeyException, InvalidAlgorithmParameterException,
            UnsupportedEncodingException {
        
        // Create the Cipher and initialize it
        Cipher c = Cipher.getInstance(PBE_ALGORITHM);
        c.init(Cipher.UNWRAP_MODE, pbekey,
                new PBEParameterSpec(salt.getBytes("UTF-8"), iterations));
        
        // Convert base64 text to raw bytes
        byte[] ctbytes = Base64.decodeBase64(ciphertext);
        
        return c.unwrap(ctbytes, RSA_KEY_TYPE, Cipher.PRIVATE_KEY);
    }
    
    /**
     * Unwraps a 5.3.x encrypted private key for migration to 5.4.x
     * 
     * @param String ciphertext Base64 ciphertext that needs to be decrypted
     * @param byte[] kek The Key Encryption Key (3DES) 
     * @return RSAPrivateKey The plaintext private key
     * 
     * @throws NoSuchPaddingException 
     * @throws NoSuchAlgorithmException 
     * @throws InvalidKeyException 
     * @throws InvalidAlgorithmParameterException 
     * @throws NoSuchProviderException 
     * 
     */
    public RSAPrivateKey unwrap53x(byte[] ciphertext, SecretKey kek) 
            throws NoSuchAlgorithmException, NoSuchPaddingException, 
            InvalidKeyException, InvalidAlgorithmParameterException
            {
        
        // Create the Cipher and initialize it
        Cipher c = Cipher.getInstance("DESede/ECB/PKCS5Padding");
        c.init(Cipher.UNWRAP_MODE, kek, c.getParameters());
        
        // Convert base64 text to raw bytes
        byte[] ctbytes = Base64.decodeBase64(ciphertext);
        
        // Return unwrapped key
        return (RSAPrivateKey) c.unwrap(ctbytes, RSA_KEY_TYPE, Cipher.PRIVATE_KEY);
    }
    
    /**
     * Wraps a private key for secure storage 
     * 
     * @param byte[] input that needs to be encrypted
     * @param byte[] pbekey The PBE key
     * @param String salt for creating a random salt for the keyspec
     * @param short iterations The number of times the keycode is processed 
     * @return String The base-64 encoded encrypted string
     * @throws NoSuchPaddingException 
     * @throws NoSuchAlgorithmException 
     * @throws InvalidKeyException 
     * @throws BadPaddingException 
     * @throws IllegalBlockSizeException 
     * @throws UnsupportedEncodingException 
     * @throws InvalidAlgorithmParameterException 
     * @exception
     */
    public String wrap(Key input, SecretKey pbekey, String salt, short iterations) 
            throws NoSuchAlgorithmException, NoSuchPaddingException, 
            InvalidKeyException, IllegalBlockSizeException, 
            InvalidAlgorithmParameterException, UnsupportedEncodingException {
        
        // Create the Cipher and initialize it
        Cipher c = Cipher.getInstance(PBE_ALGORITHM);
        c.init(Cipher.WRAP_MODE, pbekey, 
                new PBEParameterSpec(salt.getBytes("UTF-8"), iterations));
        
        // Wrap the key
        byte[] ciphertext = c.wrap(input);
        
        // Base64 encode ciphertext and return string
        return new String(Base64.encodeBase64(ciphertext));
    }
    
    /**
     * Generates a cryptographic hash of an input byte array.
     * Supports SHA-1, SHA-256, SHA-384 and SHA-512.
     * 
     * @param byte[] input that needs to be hashed
     * @param String hashtype The type of hash that needs to be
     * generated: SHA-1, SHA-256, SHA-384 or SHA-512
     * @return String A Base64-encoded hash
     * @throws NoSuchAlgorithmException if the JCEProvider does 
     * not support the implementation (this shouldn't happen 
     * because the SunJCE does support all 4) 
     */
    public String hash(byte[] input, String hashtype) 
        throws NoSuchAlgorithmException
            {
        
        // Create the MessageDigest object
        MessageDigest hash = null;
        
        // Get the correct hash instance
        if (hashtype.equals("SHA-1"))
            hash = MessageDigest.getInstance("SHA-1");
        else if (hashtype.equals("SHA-256"))
            hash = MessageDigest.getInstance("SHA-256");
        else if (hashtype.equals("SHA-384"))
            hash = MessageDigest.getInstance("SHA-384");
        else if (hashtype.equals("SHA-512"))
            hash = MessageDigest.getInstance("SHA-512");
 
        // Calculate the hash
        byte[] digest = hash.digest(input);
 
        // Base64-encode it before returning
        return new String(Base64.encodeBase64(digest));
    }
    
    /**
     * Connects to database, retrieves all Admin user encrypted passwords,
     * decrypts them using the RSA key associated with the old keycode
     * and re-encrypts them with the RSA key using the new keycode.  
     * 
     * @param String keycode The keycode of the older key used for encryption
     * @param PrivateKey oldpvk The old RSA private key used for encryption
     * @param String new keycode The keycode of the new key to be used for encryption
     * @param PrivateKey newpvk The new RSA private key used for encryption
     * @param DBPropertiesBean dbpb JDBC properties for establishing DB connection
     * @return int The number of passwords re-encrypted
     * @throws NoSuchAlgorithmException
     * @throws ClassNotFoundException 
     * @throws SQLException 
     * @throws UnsupportedEncodingException 
     * 
     */
    public int reencryptAdminPasswords(
            String oldkeycode, 
            PrivateKey oldpvk,
            String newkeycode, 
            PublicKey newpbk, 
            DBPropertiesBean dbpb) 
        throws 
            NoSuchAlgorithmException, 
            ClassNotFoundException,
            SQLException, 
            UnsupportedEncodingException {
        
        Class.forName(dbpb.getJdbcDriver());
        Connection con = Utilities.getDBConnection(dbpb);
        
        // Setup SQL to retrieve Admin passwords and keycodes
        PreparedStatement stmt = con.prepareStatement(
                "SELECT ADMIN_USER_ID, ADMIN_USER_PASSWORD, KEY_CODE, DATE_LAST_MODIFIED " +
                "FROM ADMIN_USER WHERE KEY_CODE IS NOT NULL AND ADMIN_USER_PASSWORD IS NOT NULL",
                ResultSet.TYPE_SCROLL_SENSITIVE,
                ResultSet.CONCUR_UPDATABLE);

        // Should fetchSize be fixed or let JDBC handle it?
        ResultSet rs = stmt.executeQuery();
        
        // Set up some counts
        int totalrows = 0;      // Total rows retrieved by SQL query
        int keycoderows = 0;    // Rows that matched the keycode we want
        int updatedrows = 0;    // Rows actually updated by this routine
        
        // Variables for retrieving data
        int adminid = 0;            // Admin User ID
        String currpwd = null;      // Current password
        String currkeycode = null;  // Current keycode
        
        while (rs.next()) {
            adminid = rs.getInt(1);
            currpwd = rs.getString(2);
            currkeycode = rs.getString(3);
            totalrows++;            // Increment total counter
            
            if (currkeycode.equals(oldkeycode)) {
                try{
                    keycoderows++;                                          // Increment keycode counter
                    byte[] currpwdbytes = rsaDecrypt(currpwd, oldpvk);      // Decyrpt with old key
                    rs.updateString(2, rsaEncrypt(currpwdbytes, newpbk));   // Encrypt with new key
                    rs.updateString(3, newkeycode);
                    rs.updateLong(4, new java.util.Date().getTime());
                    rs.updateRow();
                    updatedrows++;      // Increment updated counter
                    System.out.println("SUCCESS: Admin User ID " + adminid 
                            + " re-encrypted");
                } catch (IOException e) {
                    System.out.println("ERROR while re-encrypting Admin User ID " + adminid +
                            "An IOException was thrown; check logs");
                    e.printStackTrace();
                } catch (NoSuchAlgorithmException e) {
                    System.out.println("ERROR while re-encrypting Admin User ID " + adminid +
                    "An NoSuchAlgorithmException was thrown; check logs");
                    e.printStackTrace();
                } catch (InvalidKeyException e) {
                    System.out.println("ERROR while re-encrypting Admin User ID " + adminid +
                            "An InvalidKeyException was thrown; check logs");
                    e.printStackTrace();
                } catch (NoSuchPaddingException e) {
                    System.out.println("ERROR while re-encrypting Admin User ID " + adminid +
                            "An NoSuchPaddingException was thrown; check logs");
                    e.printStackTrace();
                } catch (IllegalBlockSizeException e) {
                    System.out.println("ERROR while re-encrypting Admin User ID " + adminid +
                            "An IllegalBlockSizeException was thrown; check logs");
                    e.printStackTrace();
                } catch (BadPaddingException e) {
                    System.out.println("ERROR while re-encrypting Admin User ID " + adminid +
                            "An BadPaddingException was thrown; check logs");
                    e.printStackTrace();
                } catch (NumberFormatException e) {
                    System.out.println("ERROR while re-encrypting Admin User ID " + adminid +
                            "An NumberFormatException was thrown; check logs");
                    e.printStackTrace();
                } catch (ShortBufferException e) {
                    System.out.println("ERROR while re-encrypting Admin User ID " + adminid +
                            "An ShortBufferException was thrown; check logs");
                    e.printStackTrace();
                } catch (SQLException e) {
                    System.out.println("ERROR while re-encrypting Admin User ID " + adminid +
                            "An sqlException was thrown; check logs");
                    e.printStackTrace();
                }
            }    
        }
        
        con.close();
        
        System.out.println("\nTOTAL Admin rows retrieved: " + totalrows + '\n' +
                "KEYCODE rows found: " + keycoderows + '\n' +
                "UPDATED rows: " + updatedrows + '\n');
        
        return updatedrows;
    }
    
    /**
     * Connects to database, retrieves all Customer encrypted passwords,
     * decrypts them using the RSA key associated with the old keycode
     * and re-encrypts them with the RSA key using the new keycode.  An
     * assumption being made here is that all customer passwords are
     * encrypted with the same keycode.  
     * 
     * @param String keycode The keycode of the older key used for encryption
     * @param PrivateKey oldpvk The old RSA private key used for encryption
     * @param String new keycode The keycode of the new key to be used for encryption
     * @param PrivateKey newpvk The new RSA private key used for encryption
     * @param DBPropertiesBean dbpb JDBC properties for establishing DB connection
     * @return int The number of passwords re-encrypted
     * @throws NoSuchAlgorithmException
     * @throws ClassNotFoundException 
     * @throws SQLException 
     * @throws UnsupportedEncodingException 
     * 
     */
    public int reencryptCustomerPasswords(
            String oldkeycode, 
            PrivateKey oldpvk,
            String newkeycode, 
            PublicKey newpbk, 
            DBPropertiesBean dbpb) 
        throws 
            NoSuchAlgorithmException, 
            ClassNotFoundException,
            SQLException, 
            UnsupportedEncodingException {
        
        Class.forName(dbpb.getJdbcDriver());
        Connection con = Utilities.getDBConnection(dbpb);
        
        // Setup SQL to retrieve Customer passwords and keycodes
        PreparedStatement stmt = con.prepareStatement(
                "SELECT ACCOUNT_CUSTOMER_ID, PASSWORD, KEY_CODE, DATE_MODIFIED " +
                "FROM ACCOUNT_CUSTOMER WHERE KEY_CODE IS NOT NULL AND PASSWORD IS NOT NULL",
                ResultSet.TYPE_SCROLL_SENSITIVE,
                ResultSet.CONCUR_UPDATABLE);

        // Should fetchSize be fixed or let JDBC handle it?
        ResultSet rs = stmt.executeQuery();
        
        // Set up some counts
        int totalrows = 0;      // Total rows retrieved by SQL query
        int keycoderows = 0;    // Rows that matched the keycode we want
        int updatedrows = 0;    // Rows actually updated by this routine
        
        // Variables for retrieving data
        int custid = 0;             // Customer ID
        String currpwd = null;      // Current password
        String currkeycode = null;  // Current keycode
        
        while (rs.next()) {
            custid = rs.getInt(1);
            currpwd = rs.getString(2);
            currkeycode = rs.getString(3);
            totalrows++;            // Increment total counter
            
            if (currkeycode.equals(oldkeycode)) {
                try{
                    keycoderows++;                                          // Increment keycode counter
                    byte[] currpwdbytes = rsaDecrypt(currpwd, oldpvk);      // Decyrpt with old key
                    rs.updateString(2, rsaEncrypt(currpwdbytes, newpbk));   // Encrypt with new key
                    rs.updateString(3, newkeycode);
                    rs.updateTimestamp(4, new java.sql.Timestamp(new java.util.Date().getTime()));
                    rs.updateRow();
                    updatedrows++;      // Increment updated counter
                    System.out.println("SUCCESS: Customer ID " + custid 
                            + " re-encrypted");
                } catch (IOException e) {
                    System.out.println("ERROR while re-encrypting Customer ID " + custid +
                            "An IOException was thrown; check logs");
                    e.printStackTrace();
                } catch (NoSuchAlgorithmException e) {
                    System.out.println("ERROR while re-encrypting Customer ID " + custid +
                            "An NoSuchAlgorithmException was thrown; check logs");
                    e.printStackTrace();
                } catch (InvalidKeyException e) {
                    System.out.println("ERROR while re-encrypting Customer ID " + custid +
                            "An InvalidKeyException was thrown; check logs");
                    e.printStackTrace();
                } catch (NoSuchPaddingException e) {
                    System.out.println("ERROR while re-encrypting Customer ID " + custid +
                            "An NoSuchPaddingException was thrown; check logs");
                    e.printStackTrace();
                } catch (IllegalBlockSizeException e) {
                    System.out.println("ERROR while re-encrypting Customer ID " + custid +
                            "An IllegalBlockSizeException was thrown; check logs");
                    e.printStackTrace();
                } catch (BadPaddingException e) {
                    System.out.println("ERROR while re-encrypting Customer ID " + custid +
                            "An BadPaddingException was thrown; check logs");
                    e.printStackTrace();
                } catch (NumberFormatException e) {
                    System.out.println("ERROR while re-encrypting Customer ID " + custid +
                            "An NumberFormatException was thrown; check logs");
                    e.printStackTrace();
                } catch (ShortBufferException e) {
                    System.out.println("ERROR while re-encrypting Customer ID " + custid +
                            "An ShortBufferException was thrown; check logs");
                    e.printStackTrace();
                } catch (SQLException e) {
                    System.out.println("ERROR while re-encrypting Customer ID " + custid +
                            "An sqlException was thrown; check logs");
                    e.printStackTrace();
                }
            }    
        }
        
        con.close();
        
        System.out.println("\nTOTAL customer rows retrieved: " + totalrows + '\n' +
                "KEYCODE rows found: " + keycoderows + '\n' +
                "UPDATED rows: " + updatedrows + '\n');
        
        return updatedrows;
    }
    
    /**
     * Connects to database, retrieves all encrypted Credit Card Numbers
     * (also known as Personal Account Number or PAN) from the 
     * Order_Payment_Credit_Card table, decrypts them using the RSA key 
     * associated with the old keycode and re-encrypts them with the RSA 
     * key using the new keycode.  An assumption being made here is that 
     * all CCN are encrypted with the same keycode before the migration.  
     * 
     * @param String keycode The keycode of the older key used for encryption
     * @param PrivateKey oldpvk The old RSA private key used for encryption
     * @param String new keycode The keycode of the new key to be used for encryption
     * @param PrivateKey newpvk The new RSA private key used for encryption
     * @param DBPropertiesBean dbpb JDBC properties for establishing DB connection
     * @return int The number of CCN records re-encrypted
     * @throws NoSuchAlgorithmException
     * @throws ClassNotFoundException 
     * @throws SQLException 
     * @throws UnsupportedEncodingException 
     * 
     */
    public int reencryptCreditCardNumbers(
            String oldkeycode, 
            PrivateKey oldpvk,
            String newkeycode, 
            PublicKey newpbk, 
            DBPropertiesBean dbpb) 
        throws 
            NoSuchAlgorithmException, 
            ClassNotFoundException,
            SQLException, 
            UnsupportedEncodingException {
        
        Class.forName(dbpb.getJdbcDriver());
        Connection con = Utilities.getDBConnection(dbpb);
        
        // Setup SQL to retrieve Customer passwords and keycodes
        PreparedStatement stmt = con.prepareStatement(
                "SELECT ORDER_PAYMENT_ID, CC_NUMBER, KEY_CODE FROM " +
                "ORDER_PAYMENT_CREDIT_CARD WHERE KEY_CODE IS NOT NULL " +
                "AND CC_NUMBER IS NOT NULL",
                ResultSet.TYPE_SCROLL_SENSITIVE,
                ResultSet.CONCUR_UPDATABLE);

        // Should fetchSize be fixed or let JDBC handle it?
        ResultSet rs = stmt.executeQuery();
        
        // Set up some counts
        int totalrows = 0;      // Total rows retrieved by SQL query
        int keycoderows = 0;    // Rows that matched the keycode we want
        int updatedrows = 0;    // Rows actually updated by this routine
        
        // Variables for retrieving data
        int ordpmtid = 0;           // Order Payment ID
        String currccn = null;      // Current Credit Card Number (CCN)
        String currkeycode = null;  // Current keycode
        
        while (rs.next()) {
            ordpmtid = rs.getInt(1);
            currccn  = rs.getString(2);
            currkeycode = rs.getString(3);
            totalrows++;            // Increment total counter
            
            if (currkeycode.equals(oldkeycode)) {
                try{
                    
                    keycoderows++;                                          // Increment keycode counter
                    byte[] currccnbytes = rsaDecrypt(currccn, oldpvk);      // Decyrpt with old key
                    rs.updateString(2, rsaEncrypt(currccnbytes, newpbk));   // Encrypt with new key
                    rs.updateString(3, newkeycode);
                    rs.updateRow();
                    updatedrows++;      // Increment updated counter
                    System.out.println("SUCCESS: Order Payment ID " + ordpmtid 
                            + " re-encrypted");
                } catch (IOException e) {
                    System.out.println("ERROR while re-encrypting Order Payment ID " + ordpmtid +
                            "An IOException was thrown; check logs");
                    e.printStackTrace();
                } catch (NoSuchAlgorithmException e) {
                    System.out.println("ERROR while re-encrypting Order Payment ID " + ordpmtid +
                            "An NoSuchAlgorithmException was thrown; check logs");
                    e.printStackTrace();
                } catch (InvalidKeyException e) {
                    System.out.println("ERROR while re-encrypting Order Payment ID " + ordpmtid +
                            "An InvalidKeyException was thrown; check logs");
                    e.printStackTrace();
                } catch (NoSuchPaddingException e) {
                    System.out.println("ERROR while re-encrypting Order Payment ID " + ordpmtid +
                            "An NoSuchPaddingException was thrown; check logs");
                    e.printStackTrace();
                } catch (IllegalBlockSizeException e) {
                    System.out.println("ERROR while re-encrypting Order Payment ID " + ordpmtid +
                            "An IllegalBlockSizeException was thrown; check logs");
                    e.printStackTrace();
                } catch (BadPaddingException e) {
                    System.out.println("ERROR while re-encrypting Order Payment ID " + ordpmtid +
                            "An BadPaddingException was thrown; check logs");
                    e.printStackTrace();
                } catch (NumberFormatException e) {
                    System.out.println("ERROR while re-encrypting Order Payment ID " + ordpmtid +
                            "An NumberFormatException was thrown; check logs");
                    e.printStackTrace();
                } catch (ShortBufferException e) {
                    System.out.println("ERROR while re-encrypting Order Payment ID " + ordpmtid +
                            "An ShortBufferException was thrown; check logs");
                    e.printStackTrace();
                } catch (SQLException e) {
                    System.out.println("ERROR while re-encrypting Order Payment ID " + ordpmtid +
                            "An sqlException was thrown; check logs");
                    e.printStackTrace();
                }
            }    
        }
        
        con.close();
        
        System.out.println("\nTOTAL order payment rows retrieved: " + totalrows + '\n' +
                "KEYCODE rows found: " + keycoderows + '\n' +
                "UPDATED rows: " + updatedrows + '\n');
        
        return updatedrows;
    }
}
